<?php
/**
 * Stylet Classifield - PHP Template Engine
 *
 * @category     Stylet
 * @package      Classifield
 * @author       Jano Chen <janochen@gmail.com>
 * @copyright    Copyright (c) 2008-2009 Jano Chen
 * @link         http://code.google.com/p/classifield/
 * @license      http://code.google.com/p/classifield/source/browse/LICENSE.txt  New BSD License
 * @link         $HeadURL$
 * @version      $Revision$
 * @modifiedby   $LastChangedBy$
 * @lastmodified $Date$
 */
class Classifield {
  const APPEND_LAST_CHILD = 0;
  const APPEND_FIRST_CHILD = 1;
  const APPEND_BEFORE_NODE = 2;
  const APPEND_AFTER_NODE = 3;

  const PHP_OPENING_TAG = '<script language="php">';
  const PHP_CLOSING_TAG = '</script>';

  /**
   * Classifield 模板語法的前綴，可替換
   * @var string
   */
  public static $prefix = 'php';

  /**
   * Classifield 模板語法的關鍵字，可替換
   * @var array
   */
  public static $keywords = array(
    'foreach' => 'foreach',
    'as' => 'as',
    'echo' => 'echo',
    'print' => 'print',
    'attr' => 'attr'
  );
  public $doc;

  /**
   * is XML/XHTML or HTML
   * @var bool
   */
  public $isXml = false;

  /**
   * 自動增加 HTML 元素的 id 屬性
   * @var bool
   */
  public $autoIdentify = false;

  /**
   * 在編譯模板時移除在 class 屬性裡的 Classifield 模板語法
   * @var bool
   */
  public $removeInstructions = false;
  /**
   * @var string;
   */
  public $templateFileName;
  public $binaryFile;

  /**
   * @param string $templateFileName default = null
   */
  function __construct($templateFileName = null) {
    if ($templateFileName) {
      $this->templateFileName = $templateFileName;
      if (!$this->binaryFile) {
        $this->binaryFile = $templateFileName . '.php';
      }
      $this->loadFile($this->templateFileName);
      $this->walkThrough();
      $this->save();
    }
  }
  /**
   * 載入模板
   * $doc->loadHTMLFile + $doc->save 既有的 DOMProcessingInstruction 結尾會多一個問號
   * @param string $templateFileName
   * @return bool
   */
  function loadFile($templateFileName) {
    $result = false;
    $this->doc = new DOMDocument();
    if ($this->isXml) {
      $result = $this->doc->load($templateFileName);
    } else {
      $result = $this->doc->loadHTMLFile($templateFileName);
    }
    return $result;
  }
  /**
   * 如果使用 $doc->saveHTMLFile ，新增的 DOMProcessingInstruction 結尾會少一個問號
   */
  function save($string = null) {
    if ($string) {
      $this->binaryFile = $string;
    }
    if ($this->isXml) {
      $this->doc->save($this->binaryFile);
    } else {
      $this->doc->saveHTMLFile($this->binaryFile);
    }
  }

  function getVariables(&$instructions) {
    $result = '$' . array_shift($instructions);
    while (count($instructions)) {
      $instruction = array_shift($instructions);
      if (in_array($instruction, self::$keywords)) {
        array_unshift($instructions, $instruction);
        break;
      }
      $result .= "['$instruction']";
    }
    return $result;
  }
  /**
   * @todo 實做 print
   */
  function compiler($node, $instructions) {
    array_shift($instructions); // 'php-'
    $instruction = strtolower( array_shift($instructions) );
    switch ($instruction) {
      case self::$keywords['foreach'] :
        $script = 'foreach ( ' . $this->getVariables($instructions);
        array_shift($instructions); // '-as-'
        $script .= ' as ' . $this->getVariables($instructions) . ' ) {';
        $this->addScript($script, $node, self::APPEND_FIRST_CHILD);
        $this->addScript('}', $node, self::APPEND_LAST_CHILD);
        break;
      case self::$keywords['echo']:
        $script = 'echo ' . $this->getVariables($instructions) . ';';
        $this->addScript($script, $node, self::APPEND_LAST_CHILD);
        break;
      case self::$keywords['print']:
        $script = 'echo ' . $this->getVariables($instructions) . ';';
        $this->addScript($script, $node, self::APPEND_LAST_CHILD);
        break;
      case self::$keywords['assign']:
        $this->assign(array_shift($instructions), $node);
        break;
      case self::$keywords['attr']:
        $this->attr($instructions, $node);
        break;
    }
  }

  function addScript ($script, $node, $position = self::APPEND_LAST_CHILD) {
    if ($this->isXml) {
      $newNode = new DOMProcessingInstruction('php', $script);
    } else {
      $newNode = $this->doc->createDocumentFragment();
      $newNode->appendXML(self::PHP_OPENING_TAG . PHP_EOL . $script . PHP_EOL . self::PHP_CLOSING_TAG);
    }
    switch ($position) {
      case self::APPEND_FIRST_CHILD:
        $node->insertBefore($newNode, $node->firstChild);
        break;
      case self::APPEND_LAST_CHILD:
        $node->appendChild($newNode);
        break;
      case self::APPEND_BEFORE_NODE:
        $node->insertBefore($newNode, $node);
        break;
      case self::APPEND_AFTER_NODE:
        $node->insertBefore($newNode, $node->nextSibling);
        break;
      default:
        break;
    }
  }
  function attr ($instructions, $node) {
    $key = array_shift($instructions);
    $value = $this->getVariables($instructions);
    if ( $node->hasAttribute('id') ) {
      $id = $node->getAttribute('id');
      $node->removeAttribute('id');
    } else {
      $id = 'id-' . spl_object_hash($node);
    }
    $node->setAttribute("<?php echo Html::createAttributes(array('{$key}' => {$value})); ?> id", $id);
  }
  function assign ($attr, $node) {
    if ( $node->hasAttribute('id') ) {
      $id = $node->getAttribute('id');
      $node->removeAttribute('id');
    } elseif (isset($GLOBALS[$attr]['id'])) {
      $id = isset($GLOBALS[$attr]['id']);
    } else {
      $id = 'id-' . spl_object_hash($node);
    }
    $node->setAttribute("<?php echo Html::createAttributes(\${$attr}); ?> id", $id);
    $script = "if (\${$attr}['innerHtml']) { echo \${$attr}['innerHtml']; }";
    $this->addScript($script, $node, self::APPEND_LAST_CHILD);
  }

  function walkThrough () {
    $nodes = new RecursiveIteratorIterator(
      new RecursiveDOMNodeIterator($this->doc->documentElement),
      RecursiveIteratorIterator::SELF_FIRST
      );

    foreach ($nodes as $node) {
      if ( $node->nodeType != XML_ELEMENT_NODE ) {
        continue;
      }
      if ( $this->autoIdentify and !$node->hasAttribute('id')) {
        $node->setAttribute('id', 'id-' . spl_object_hash($node));
      }
      if ( !$node->hasAttribute('class') ) {
        continue;
      }
      $classes = explode(' ', $node->getAttribute('class'));
      foreach ($classes as $class) {
        $instructions = explode('-', $class);
        if ( self::$prefix === $instructions[0]) {
          $this->compiler($node, $instructions);
        }
      }
    }
  }
  /**
   * @link http://www.php.net/manual/en/language.oop5.magic.php#language.oop5.magic.tostring Magic Methods
   */
  public function __toString() {
    return $this->binaryFile;
  }
  /**
   * “自動載入類別”函式
   * <code>
   * spl_autoload_register(array('Classifield', 'autoload'));
   * </code>
   * @see spl_autoload_register()
   */
  public static function autoload ($className) {
    $fileName = dirname(__FILE__) . DIRECTORY_SEPARATOR . $className . '.class.php';

    if (file_exists($fileName)) {
      require_once $fileName;
    }
  }
}

/**
 * 註冊“自動載入類別”函式
 */
spl_autoload_register(array('Classifield', 'autoload'));